1 /**
2 Copyright: Copyright (c) 2021, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 Copied from dextool.
11 
12 Algorithm for detecting what files need to be analyzed based on previous state.
13 */
14 module code_checker.change;
15 
16 import logger = std.experimental.logger;
17 import std.exception : collectException;
18 
19 import my.path;
20 import my.optional;
21 import compile_db;
22 
23 import code_checker.database : Database, toTrackFile;
24 import code_checker.utility : toAbsoluteRoot;
25 import code_checker.cache;
26 
27 /** Returns: the root files that need to be re-analyzed because either them or
28  * their dependency has changed.
29  */
30 bool[AbsolutePath] dependencyAnalyze(ref Database db, AbsolutePath rootDir, ref FileStatCache fcache) @trusted {
31     import std.algorithm : map, cache, filter;
32     import std.datetime : dur;
33     import std.path : buildPath;
34     import std.typecons : tuple;
35     import std.math : abs;
36     import std.conv : to;
37     import miniorm : spinSql;
38     import code_checker.database : FileId, TrackFile, DepFile;
39     import my.hash : Checksum64;
40 
41     typeof(return) rval;
42 
43     // pessimistic. Add all as needing to be analyzed.
44     foreach (a; spinSql!(() => db.fileApi.getRootFiles).map!(
45             a => spinSql!(() => db.fileApi.getFile(a)).get)) {
46         auto p = buildPath(rootDir, a.file).AbsolutePath;
47         rval[p] = false;
48     }
49 
50     try {
51         auto getTrackFile = (Path p) => spinSql!(() => db.fileApi.getFile(p));
52 
53         TrackFile[Path] dbDeps;
54         foreach (a; spinSql!(() => db.dependencyApi.getAll))
55             dbDeps[a.file] = a.toTrackFile;
56 
57         bool isChanged(T)(T f) nothrow {
58             try {
59                 if (!isSame(f.root, f.root.file.AbsolutePath, fcache))
60                     return true;
61 
62                 foreach (a; f.deps.filter!(a => !isSame(dbDeps[a],
63                         toAbsoluteRoot(rootDir, a), fcache))) {
64                     logger.tracef("%s dependency changed -> %s", f.root.file,
65                             toAbsoluteRoot(rootDir, a));
66                     return true;
67                 }
68 
69                 return false;
70             } catch (Exception e) {
71                 logger.trace(e.msg).collectException;
72             }
73             return true;
74         }
75 
76         foreach (f; spinSql!(() => db.fileApi).getRootFiles
77                 .map!(a => spinSql!(() => db.fileApi.getFile(a)).get)
78                 .map!(a => tuple!("root", "deps")(a, spinSql!(() => db.dependencyApi.get(a.file))))
79                 .cache
80                 .filter!(a => isChanged(a))
81                 .map!(a => a.root.file)) {
82             rval[buildPath(rootDir, f).AbsolutePath] = true;
83         }
84     } catch (Exception e) {
85         logger.warning(e.msg);
86     }
87 
88     debug logger.trace("Dependency analyze: ", rval);
89 
90     return rval;
91 }
92 
93 /// Convert to an absolute path by finding the first match among the compiler flags
94 Optional!AbsolutePath toAbsolutePath(Path file, AbsolutePath parentDir,
95         AbsolutePath workDir, ParseFlags.Include[] includes, SystemIncludePath[] systemIncludes) @trusted nothrow {
96     import std.algorithm : map, filter;
97     import std.file : exists, isDir;
98     import std.path : buildPath;
99 
100     Optional!AbsolutePath lookup(string dir) nothrow {
101         const p = buildPath(dir, file);
102         try {
103             if (exists(p) && !isDir(p))
104                 return some(AbsolutePath(p));
105         } catch (Exception e) {
106         }
107         return none!AbsolutePath;
108     }
109 
110     {
111         auto a = lookup(parentDir.toString);
112         if (a.hasValue)
113             return a;
114     }
115 
116     {
117         auto a = lookup(workDir.toString);
118         if (a.hasValue)
119             return a;
120     }
121 
122     foreach (a; includes.map!(a => lookup(a.payload))
123             .filter!(a => a.hasValue)) {
124         return a;
125     }
126 
127     foreach (a; systemIncludes.map!(a => lookup(a.value))
128             .filter!(a => a.hasValue)) {
129         return a;
130     }
131 
132     return none!AbsolutePath;
133 }